﻿//===============================================================================
// PostSharp Poco implementation for EntityFramework
//===============================================================================
// Copyright © Ruurd Boeke.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.Objects.DataClasses;
using System.Diagnostics;
using System.Data.Objects;
using System.Data.Common;
using System.Data.Metadata.Edm;
using System.Reflection;
using System.Collections;
using PostSharp.Laos;

namespace EntityFrameworkContrib.PostSharp4EF
{
    #region interfaces we are implementing
    /// <summary>
    /// Interface that is implemented on the object that allows us to work with a context
    /// </summary>
    public interface IContextAware
    {
        /// <summary>
        /// Attaches an object and all related objects to a context.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="attachOption">The attach option.</param>
        /// <param name="GetEntitySet">The get entity set delegate, which will determine the entitysetname</param>
        void AttachGraphToContext(ObjectContext context, Func<object, string> GetEntitySet);

        /// <summary>
        /// Uses the context to find the 'original values' and load these into the 
        /// loaded state of the object. Do this right before serializing so that the
        /// serialized version of this object is correct.
        /// </summary>
        /// <param name="context">The context.</param>
        void CreateSerializableState(ObjectContext context);
    }


    /// <summary>
    /// a facade for the interfaces needed to get to iPoco
    /// </summary>
    public interface IPocoFacade : IEntityWithChangeTracker, IEntityWithKey, IEntityWithRelationships, IGetChangeTracker, IContextAware
    {
    } 
    #endregion

    #region hookup implementation object to the type
    /// <summary>
    /// Implements the 3 interfaces needed on an entity
    /// </summary>
    [Serializable]
    sealed class PocoEntityInterfacesSubAspect : CompositionAspect
    {

        public override object CreateImplementationObject(InstanceBoundLaosEventArgs eventArgs)
        {
            return new PocoEntityImplementation(eventArgs.Instance);
        }

        public override Type GetPublicInterface(Type containerType)
        {
            return typeof(IPocoFacade);
        }

        /// <summary>
        /// Gets weaving options.
        /// </summary>
        /// <returns>Weaving options specifying that the implementation accessor interface (<see cref="IComposed{T}"/>)
        /// should be exposed, and that the implementation of interfaces should be silently ignored if they are
        /// already implemented in the parent types.</returns>
        public override CompositionAspectOptions GetOptions()
        {
            return
                CompositionAspectOptions.GenerateImplementationAccessor |
                CompositionAspectOptions.IgnoreIfAlreadyImplemented;
        }
    } 
    #endregion

    /// <summary>
    /// the implementation object used to marshal calls to the 3 ipoco interfaces to
    /// </summary>
    [Serializable]
    class PocoEntityImplementation : IPocoFacade
    {
        #region fields and properties
        /// <summary>
        /// object we are implementing for
        /// </summary>
        private readonly object instance;

        /// <summary>
        /// allows us to skip the CreateSerializableState if we are already working on serializing
        /// </summary>
        bool IsCreatingSerializableState = false;

        #endregion

        #region ctor
        /// <summary>
        /// Initializes a new instance of the <see cref="PocoEntityImplementation"/> class.
        /// </summary>
        /// <param name="instance">The instance.</param>
        public PocoEntityImplementation(object instance)
        {
            this.instance = instance;
        } 
        #endregion

        #region key: Implementation for IEntityWithKey
        EntityKey _entityKey = null;

        // Define the EntityKey property for the class.
        EntityKey IEntityWithKey.EntityKey
        {
            get
            {
                return _entityKey;
            }
            set
            {
                // Set the EntityKey property, if it is not set.
                // Report the change if the change tracker exists.
                if (_changeTracker != null)
                {
                    _changeTracker.EntityMemberChanging("-EntityKey-");
                    _entityKey = value;
                    _changeTracker.EntityMemberChanged("-EntityKey-");
                }
                else
                {
                    _entityKey = value;
                }
            }
        }
        #endregion

        #region change tracking: Implementation for IEntityChangeTracker and IGetChangeTracker
        IEntityChangeTracker _changeTracker = null;

        IEntityChangeTracker IGetChangeTracker.GetChangeTracker()
        {
            return _changeTracker;
        }


        // Specify the IEntityChangeTracker to use for tracking changes.
        void IEntityWithChangeTracker.SetChangeTracker(IEntityChangeTracker changeTracker)
        {
            _changeTracker = changeTracker;

            // Every time the change tracker is set, we must also set all the 
            // complex type change trackers.
            ((IUpdateComplexTypesWithTracker)instance).UpdateComplexTypesWithTracker(changeTracker);
        }

        public void ReportPropertyChanging(string propertyName)
        {
            if (_changeTracker != null)
                _changeTracker.EntityMemberChanging(propertyName);
        }
        public void ReportPropertyChanged(string propertyName)
        {
            if (_changeTracker != null)
                _changeTracker.EntityMemberChanged(propertyName);
        }
        #endregion

        #region relationships: Implementation for IEntityWithRelationships
        RelationshipManager _relationships = null;

        // Define a relationship manager for the class.
        RelationshipManager IEntityWithRelationships.RelationshipManager
        {
            get
            {
                // TODO: this might not be correct
                if (null == _relationships)
                    _relationships = RelationshipManager.Create((IEntityWithRelationships)instance);
                return _relationships;
            }
        }
        #endregion

        #region IContextAware
        /// <summary>
        /// Uses the context to find the 'original values' and load these into the
        /// loaded state of the object. Do this right before serializing so that the
        /// serialized version of this object is correct.
        /// </summary>
        /// <param name="context">The context.</param>
        void IContextAware.CreateSerializableState(ObjectContext context)
        {
            if (IsCreatingSerializableState)
                return; // already busy, would be called from the other end of relationship

            IsCreatingSerializableState = true;

            ISerializationHelp eS = this.instance as ISerializationHelp;
            IEntityWithKey eK = this.instance as IEntityWithKey;

            Debug.Assert(eS != null, "Instance must implement ISerializationHelp");
            Debug.Assert(eK != null, "Instance must implement IEntityWithKey");

            EntityKey sourceKey = eK.EntityKey;

            // This method will throw if the key is null or an entry isn't found to match it.  We
            // could throw nicer exceptions, but this will catch the important invalid cases.
            ObjectStateEntry sourceStateEntry = context.ObjectStateManager.GetObjectStateEntry(sourceKey);


            if (sourceStateEntry.State == EntityState.Detached)
            {
                throw new InvalidOperationException("Can't get original values when detached.");
            }


            EntityType t = context.MetadataWorkspace.GetItem<EntityType>(this.instance.GetType().FullName, DataSpace.CSpace);

            if (sourceStateEntry.State != EntityState.Added)
            {
                DbDataRecord record = sourceStateEntry.OriginalValues;

                // the normal values
                foreach (EdmProperty prop in t.Properties)
                {
                    object value = record[prop.Name];

                    eS.SetValueAsOriginal(prop.Name, value);
                }
            }

            // relations
            foreach (var relationshipGroup in context.GetRelationshipsByRelatedEnd(eK, 
                EntityState.Deleted | EntityState.Unchanged | EntityState.Modified | EntityState.Added))
            {
                IRelatedEnd tgtRelatedEnd = (IRelatedEnd)relationshipGroup.Key;

                // need to relate this to a navigational property
                var navPropList = from navprop in t.NavigationProperties
                               where navprop.RelationshipType.FullName == tgtRelatedEnd.RelationshipName &&
                                    navprop.Name == tgtRelatedEnd.TargetRoleName
                               select navprop;
                NavigationProperty navProp = navPropList.First();

                if (tgtRelatedEnd.IsEntityReference())
                {
                    #region is a singular object
                    foreach (ObjectStateEntry srcEntry in relationshipGroup)
                    {
                        IEntityWithKey OtherEnd = context.GetEntityByKey(srcEntry.OtherEndKey(sourceKey));

                        // if it was an original value, put it in a list that we can store as original values
                        if (srcEntry.State == EntityState.Deleted || srcEntry.State == EntityState.Unchanged)
                        {
                            // this was an original value
                            eS.SetValueAsOriginal(navProp.Name, OtherEnd); 
                        }

                        // in any way, do notify the object to also create a serializable version
                        if (OtherEnd is IContextAware)
                        {
                            ((IContextAware)OtherEnd).CreateSerializableState(context);
                        }
                    }
                    #endregion
                }
                else
                {

                    #region is a collection
                    // TODO: hack, don't have the time to find out how I see what type it is
                    // I can't find a way to get to the field type, even though we have a field.. that is generic and
                    // somehow I can not get the generic type out of it
                    TypeUsage typeUsage = navProp.ToEndMember.MetadataProperties.First(prop => prop.Name == "TypeUsage").Value as TypeUsage;
                    string toMemberTypeName = typeUsage.EdmType.Name.Replace("reference[", "").Replace("]", "");
                    Type target = this.instance.GetType().Assembly.GetType(toMemberTypeName);


                    // TODO: find a way to circumvent this call
                    Type generic = Type.GetType("System.Collections.Generic.List`1").MakeGenericType(new Type[] { target });
                    IList origValues = (IList)Activator.CreateInstance(generic);

                    foreach (ObjectStateEntry srcEntry in relationshipGroup)
                    {
                        IEntityWithKey OtherEnd = context.GetEntityByKey(srcEntry.OtherEndKey(sourceKey));

                        // if it was an original value, put it in a list that we can store as original values
                        if (srcEntry.State == EntityState.Deleted || srcEntry.State == EntityState.Unchanged)
                        {
                            // this was an original value
                            origValues.Add(OtherEnd);
                        }

                        // in any way, do notify the object to also create a serializable version
                        if (OtherEnd is IContextAware)
                        {
                            ((IContextAware)OtherEnd).CreateSerializableState(context);
                        }
                    }


                    eS.SetValueAsOriginal(navProp.Name, origValues); 
                    #endregion
                }
            }

            IsCreatingSerializableState = false;
        }

        #region original attach graph
        /// <summary>
        /// Attaches an object and all related objects to a context.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="GetEntitySet">The get entity set delegate, which will determine the entitysetname</param>
        //void IContextAware.AttachGraphToContext(ObjectContext context, Func<object, string> GetEntitySet)
        //{
        //    #region setup
        //    if (((ISerializationHelp)instance).IsDeserializing())
        //        return;

        //    IStorageAccess eSa = (IStorageAccess)this.instance;
        //    ISerializationHelp eS = (ISerializationHelp)this.instance;
        //    IEntityWithKey eK = (IEntityWithKey)this.instance;

        //    if (eK.EntityKey != null)
        //        return; // only attach once

        //    eS.BeginDeserialization();




        //    // there is no way to know the entityset name. An entity could even live in different sets, 
        //    // so we use a callback to let the user decide in which set this object lives
        //    string fullEntitySetName = context.DefaultContainerName + "." + GetEntitySet(this.instance);

        //    // set all the current values
        //    EntityType t = context.MetadataWorkspace.GetItem<EntityType>(this.instance.GetType().FullName, DataSpace.CSpace);
        //    #endregion

        //    #region attach or add
        //    bool added = false;
        //    foreach (EdmMember keyMember in t.KeyMembers)
        //    {
        //        if (eS.GetOriginalValue(keyMember.Name).GetHashCode() == 0)  // so a key was not set
        //        {
        //            context.AddObject(GetEntitySet(this.instance), this.instance);
        //            added = true;
        //            break;
        //        }
        //    }
        //    if (!added)
        //    {
        //        context.AttachTo(GetEntitySet(this.instance), eK);
        //    }
        //    #endregion
        //    EntityKey key = context.GetEntityKey(GetEntitySet(instance), instance);

        //    ObjectStateEntry sourceStateEntry = context.ObjectStateManager.GetObjectStateEntry(key);
        //    CurrentValueRecord currecord = sourceStateEntry.CurrentValues;

        //    #region setting values on the record
        //    // since setting the currecord[i] to a value will overwrite our currentvalue
        //    // we are going to use the fastest store we have: the originalvalue setter
        //    for (int i = 0; i < currecord.FieldCount; i++)
        //    {
        //        string propname = currecord.GetName(i);
        //        if (t.KeyMembers.FirstOrDefault(edm => edm.Name == propname) != null)
        //            continue;   // this was a key                

        //        object originalValue = eS.GetOriginalValue(propname);
        //        object currentValue = eSa.GetValue(propname);

        //        // set the currentvalue to the original value. If this is too confusing, we might
        //        // consider a list or something.
        //        eS.SetValueAsOriginal(propname, currentValue);

        //        // the original value could be null
        //        if (originalValue == null)
        //        {
        //            // TODO: faster way to set to default value, maybe a switch on all types

        //            // only need to do if currentvalue is not null
        //            if (currentValue != null)
        //            {
        //                Type emptytype = currentValue.GetType();
        //                if (emptytype.IsValueType)
        //                    currecord.SetValue(i, Activator.CreateInstance(currentValue.GetType()));
        //                else
        //                {
        //                    // TODO: need this fixed correctly

        //                    if (emptytype.IsAssignableFrom(typeof(string)))
        //                        currecord.SetValue(i, String.Empty);
        //                    else
        //                        currecord.SetValue(i, DBNull.Value);    // it's a reference, so we can set null

        //                }
        //            }
        //        }
        //        else
        //            currecord.SetValue(i, originalValue);
        //    }
        //    #endregion

        //    sourceStateEntry.AcceptChanges();

        //    #region setting the currentvalues and reporting changes
        //    for (int i = 0; i < currecord.FieldCount; i++)
        //    {
        //        string propname = currecord.GetName(i);

        //        if (t.KeyMembers.FirstOrDefault(edm => edm.Name == propname) != null)
        //            continue;   // this was a key

        //        // don't get confused: we pulled a switch trick 
        //        object originalValue = eSa.GetValue(propname);
        //        object currentValue = eS.GetOriginalValue(propname);

        //        if (originalValue != currentValue)
        //        {
        //            currecord.SetValue(i, currentValue);

        //            sourceStateEntry.SetModifiedProperty(propname);
        //        }
        //    }
        //    #endregion


        //    #region relationships
        //    foreach (NavigationProperty nav in t.NavigationProperties)
        //    {
        //        object originalValue = eS.GetOriginalValue(nav.Name);
        //        object currentValue = eSa.GetValue(nav.Name);

        //        IList originalValues = originalValue as IList;
        //        IList currentValues = currentValue as IList;

        //        if (originalValues != null)
        //        {
        //            // it's a relatedcollection
        //            #region work through all unchanged or deleted relations and attach or delete
        //            foreach (object orig in originalValues)
        //            {
        //                // a. make sure it is attached
        //                ((IContextAware)orig).AttachGraphToContext(context, GetEntitySet);


        //                /* [deletes] Deletes are difficult because on *.* relations they might have been deleted
        //                 * from the other side already (because of the call to attachGraph we just did).
        //                 * So we have to check if this object has been deleted */
        //                if (sourceStateEntry.State == EntityState.Deleted)
        //                {
        //                    // already deleted. no need to go through with this
        //                    continue;
        //                }

        //                // b. attach relation
        //                RelationshipManager manager = ((IEntityWithRelationships)orig).RelationshipManager;
        //                IRelatedEnd relatedEnd = manager.GetRelatedEnd(nav.RelationshipType.Name, nav.FromEndMember.Name);  // FromEndMember is this instance

        //                // maybe already attached
        //                if (!relatedEnd.Contains(eK.EntityKey))
        //                {
        //                    relatedEnd.Attach((IEntityWithRelationships)this.instance);
        //                }


        //                if (!currentValues.Contains(orig))
        //                {
        //                    // c. delete object
        //                    //context.DeleteObject(orig);
        //                    relatedEnd.Remove((IEntityWithRelationships)this.instance);
        //                }

        //            }
        //            #endregion
        //        }

        //        if (currentValues != null)
        //        {
        //            #region work through all new relations and add
        //            foreach (object cur in currentValues)
        //            {
        //                if (!originalValues.Contains(cur))
        //                {
        //                    // [added]

        //                    // a. need to attach so we can work through it's relations like normal
        //                    ((IContextAware)cur).AttachGraphToContext(context, GetEntitySet);


        //                    // TODO: build support for *.* relations.. this only works for 1.*

        //                    // b. attach relation
        //                    RelationshipManager manager = ((IEntityWithRelationships)cur).RelationshipManager;
        //                    IRelatedEnd relatedEnd = manager.GetRelatedEnd(nav.RelationshipType.Name, nav.FromEndMember.Name);  // FromEndMember is this instance
        //                    // maybe already attached
        //                    if (!relatedEnd.Contains(eK.EntityKey))
        //                    {
        //                        relatedEnd.Add((IEntityWithRelationships)this.instance);
        //                    }
        //                }
        //            }
        //            #endregion
        //        }

        //        if (originalValue != null && !(originalValue is IList))
        //        {
        //            // it's a reference to another entity
        //            ((IContextAware)originalValue).AttachGraphToContext(context, GetEntitySet);
        //        }

        //        if (originalValue != currentValue && currentValue != null && !(currentValue is IList))
        //        {
        //            ((IContextAware)currentValue).AttachGraphToContext(context, GetEntitySet);
        //        }
        //    }
        //    #endregion


        //    ((ISerializationHelp)instance).EndDeserialization();
        //}

        #endregion

        void IContextAware.AttachGraphToContext(ObjectContext context, Func<object, string> GetEntitySet)
        {
            IEntityWithKey eK = (IEntityWithKey)this.instance;

            // entry for a complete graph. This is the root object
            if (eK.EntityKey != null)
                throw new InvalidOperationException("Don't try to attach a graph twice");

            // recursion won't work too well for some situations, so we register each entity
            // in this list
            List<object> entitiesInGraph = new List<object>();

            // work throug the complete graph, starting with the root
            // not hooking up relations yet, but make sure all objects are connected to the context
            AttachOrAddToContext(context, this.instance, GetEntitySet, entitiesInGraph);

            foreach (object entity in entitiesInGraph)
            {
                FixRelations(context, entity, GetEntitySet);
            }

            foreach (object entity in entitiesInGraph)
            {
                ((ISerializationHelp)entity).EndDeserialization();
            }

        }

        void AttachOrAddToContext(ObjectContext context, object Entity, Func<object, string> GetEntitySet, List<object> entitiesInGraph)
        {
            if (((ISerializationHelp)Entity).IsDeserializing())
                return;

            IStorageAccess eSa = (IStorageAccess)Entity;
            ISerializationHelp eS = (ISerializationHelp)Entity;
            IEntityWithKey eK = (IEntityWithKey)Entity;

            if (eK.EntityKey != null)
                return; // only attach once

            eS.BeginDeserialization();

            entitiesInGraph.Add(Entity);


            // there is no way to know the entityset name. An entity could even live in different sets, 
            // so we use a callback to let the user decide in which set this object lives
            string fullEntitySetName = context.DefaultContainerName + "." + GetEntitySet(Entity);

            // set all the current values
            EntityType t = context.MetadataWorkspace.GetItem<EntityType>(Entity.GetType().FullName, DataSpace.CSpace);

            #region attach or add to context
            bool added = false;
            foreach (EdmMember keyMember in t.KeyMembers)
            {
                if (eS.GetOriginalValue(keyMember.Name).GetHashCode() == 0)  // so a key was not set
                {
                    context.AddObject(GetEntitySet(Entity), Entity);
                    added = true;
                    break;
                }
            }
            if (!added)
            {
                context.AttachTo(GetEntitySet(Entity), eK);
            }
            #endregion

            EntityKey key = context.GetEntityKey(GetEntitySet(Entity), Entity);

            ObjectStateEntry sourceStateEntry = context.ObjectStateManager.GetObjectStateEntry(key);
            CurrentValueRecord currecord = sourceStateEntry.CurrentValues;

            #region setting values on the record
            // since setting the currecord[i] to a value will overwrite our currentvalue
            // we are going to use the fastest store we have: the originalvalue setter
            for (int i = 0; i < currecord.FieldCount; i++)
            {
                string propname = currecord.GetName(i);
                if (t.KeyMembers.FirstOrDefault(edm => edm.Name == propname) != null)
                    continue;   // this was a key                

                object originalValue = eS.GetOriginalValue(propname);
                object currentValue = eSa.GetValue(propname);

                // set the currentvalue to the original value. If this is too confusing, we might
                // consider a list or something.
                eS.SetValueAsOriginal(propname, currentValue);

                // so, only set original values if this is not an added object
                if (!added)
                {

                    // the original value could be null
                    if (originalValue == null)
                    {
                        // TODO: faster way to set to default value, maybe a switch on all types

                        // only need to do if currentvalue is not null
                        if (currentValue != null)
                        {
                            Type emptytype = currentValue.GetType();
                            if (emptytype.IsValueType)
                                currecord.SetValue(i, Activator.CreateInstance(currentValue.GetType()));
                            else
                            {
                                // TODO: need this fixed correctly

                                if (emptytype.IsAssignableFrom(typeof(string)))
                                    currecord.SetValue(i, String.Empty);
                                else
                                    currecord.SetValue(i, DBNull.Value);    // it's a reference, so we can set null

                            }
                        }
                    }
                    else
                        currecord.SetValue(i, originalValue);
                }
            }
            #endregion

            if (!added)
            {   
                // only if not added
                sourceStateEntry.AcceptChanges();
            }

            #region setting the currentvalues and reporting changes
            for (int i = 0; i < currecord.FieldCount; i++)
            {
                string propname = currecord.GetName(i);

                if (t.KeyMembers.FirstOrDefault(edm => edm.Name == propname) != null)
                    continue;   // this was a key

                // don't get confused: we pulled a switch trick 
                object originalValue = eSa.GetValue(propname);
                object currentValue = eS.GetOriginalValue(propname);

                if (originalValue != currentValue)
                {
                    currecord.SetValue(i, currentValue);

                    if (!added)
                    {
                        // if we are added, everything is clearly a modified property
                        sourceStateEntry.SetModifiedProperty(propname);
                    }

                    // switch back
                    eS.SetValueAsOriginal(propname, currentValue);
                }
            }
            #endregion

            #region run through relations and attach them as well
            foreach (NavigationProperty nav in t.NavigationProperties)
            {
                object originalValue = eS.GetOriginalValue(nav.Name);
                object currentValue = eSa.GetValue(nav.Name);

                IList originalValues = originalValue as IList;
                IList currentValues = currentValue as IList;

                if (originalValues != null)
                {
                    // it's a relatedcollection
                    foreach (object orig in originalValues)
                    {
                        AttachOrAddToContext(context, orig, GetEntitySet, entitiesInGraph);
                    }
                }

                if (currentValues != null)
                {
                    foreach (object cur in currentValues)
                    {
                        if (originalValues == null || !originalValues.Contains(cur))
                        {
                            AttachOrAddToContext(context, cur, GetEntitySet, entitiesInGraph);
                        }
                    }
                }

                if (originalValue != null && !(originalValue is IList))
                {
                    AttachOrAddToContext(context, originalValue, GetEntitySet, entitiesInGraph);
                }

                if (originalValue != currentValue && currentValue != null && !(currentValue is IList))
                {
                    AttachOrAddToContext(context, currentValue, GetEntitySet, entitiesInGraph);
                }
            }
            #endregion

        }

        void FixRelations(ObjectContext context, object Entity, Func<object, string> GetEntitySet)
        {
            IStorageAccess eSa = (IStorageAccess)Entity;
            ISerializationHelp eS = (ISerializationHelp)Entity;
            IEntityWithKey eK = (IEntityWithKey)Entity;

            // there is no way to know the entityset name. An entity could even live in different sets, 
            // so we use a callback to let the user decide in which set this object lives
            string fullEntitySetName = context.DefaultContainerName + "." + GetEntitySet(Entity);

            // set all the current values
            EntityType t = context.MetadataWorkspace.GetItem<EntityType>(Entity.GetType().FullName, DataSpace.CSpace);

            foreach (NavigationProperty nav in t.NavigationProperties)
            {
                object originalValue = eS.GetOriginalValue(nav.Name);
                object currentValue = eSa.GetValue(nav.Name);

                IList originalValues = eS.GetOriginalValue(nav.Name) as IList;
                IList currentValues = eSa.GetValue(nav.Name) as IList;

                if (originalValues != null)
                {
                    // it's a relatedcollection
                    #region work through all unchanged or deleted relations and attach or delete
                    foreach (object orig in originalValues)
                    {
                        // b. attach relation
                        RelationshipManager manager = ((IEntityWithRelationships)orig).RelationshipManager;
                        IRelatedEnd relatedEnd = manager.GetRelatedEnd(nav.RelationshipType.Name, nav.FromEndMember.Name);  // FromEndMember is this instance

                        // maybe already attached
                        if (!relatedEnd.Contains(eK.EntityKey))
                        {
                            relatedEnd.Attach((IEntityWithRelationships)Entity);
                        }


                        if (!currentValues.Contains(orig))
                        {
                            // c. delete relationship
                            relatedEnd.Remove((IEntityWithRelationships)Entity);
                        }

                    }
                    #endregion
                }

                if (currentValues != null)
                {
                    #region work through all new relations and add
                    foreach (object cur in currentValues)
                    {
                        if (originalValues == null || !originalValues.Contains(cur))
                        {
                            // [added]
                            // TODO: build support for *.* relations.. this only works for 1.*

                            // b. attach relation
                            RelationshipManager manager = ((IEntityWithRelationships)cur).RelationshipManager;
                            IRelatedEnd relatedEnd = manager.GetRelatedEnd(nav.RelationshipType.Name, nav.FromEndMember.Name);  // FromEndMember is this instance
                            // maybe already attached
                            if (!relatedEnd.Contains(eK.EntityKey))
                            {
                                relatedEnd.Add((IEntityWithRelationships)Entity);
                            }
                        }

                    }
                    #endregion
                }

                if (originalValue != null && !(originalValue is IList))
                {
                    RelationshipManager manager = ((IEntityWithRelationships)originalValue).RelationshipManager;
                    IRelatedEnd relatedEnd = manager.GetRelatedEnd(nav.RelationshipType.Name, nav.FromEndMember.Name);  // FromEndMember is this instance
                    // maybe already attached
                    if (!relatedEnd.Contains(eK.EntityKey))
                    {
                        relatedEnd.Attach((IEntityWithRelationships)Entity);
                    }

                    if (currentValue != originalValue)
                    {
                        // so relationship was deleted
                        relatedEnd.Remove((IEntityWithRelationships)Entity);

                    }
                }

                if (originalValue != currentValue && currentValue != null && !(currentValue is IList))
                {
                    RelationshipManager manager = ((IEntityWithRelationships)currentValue).RelationshipManager;
                    IRelatedEnd relatedEnd = manager.GetRelatedEnd(nav.RelationshipType.Name, nav.FromEndMember.Name);  // FromEndMember is this instance
                    // maybe already attached
                    if (!relatedEnd.Contains(eK.EntityKey))
                    {
                        relatedEnd.Add((IEntityWithRelationships)Entity);
                    }
                }

            }

        }

        #endregion
    }

    #region extension to context
    public static class UtilityExtensionMethods
    {
        //
        // just type a little less
        //

        public static EntityState GetEntityState(this ObjectContext context, EntityKey key)
        {
            var entry = context.ObjectStateManager.GetObjectStateEntry(key);
            return entry.State;
        }

        public static string GetFullEntitySetName(this EntityKey key)
        {
            return key.EntityContainerName + "." + key.EntitySetName;
        }

        public static IEntityWithKey GetEntityByKey(this ObjectContext context, EntityKey key)
        {
            return (IEntityWithKey)context.ObjectStateManager.GetObjectStateEntry(key).Entity;
        }

        //
        // ObjectStateEntry
        //

        public static IExtendedDataRecord UsableValues(this ObjectStateEntry entry)
        {
            switch (entry.State)
            {
                case EntityState.Added:
                case EntityState.Detached:
                case EntityState.Unchanged:
                case EntityState.Modified:
                    return (IExtendedDataRecord)entry.CurrentValues;
                case EntityState.Deleted:
                    return (IExtendedDataRecord)entry.OriginalValues;
                default:
                    throw new InvalidOperationException("This entity state should not exist.");
            }
        }

        public static EdmType EdmType(this ObjectStateEntry entry)
        {
            return entry.UsableValues().DataRecordInfo.RecordType.EdmType;
        }

        public static bool IsManyToMany(this AssociationType associationType)
        {
            foreach (RelationshipEndMember endMember in associationType.RelationshipEndMembers)
            {
                if (endMember.RelationshipMultiplicity != RelationshipMultiplicity.Many)
                {
                    return false;
                }
            }
            return true;
        }

        //
        // RelationshipEntry
        //

        public static bool IsRelationshipForKey(this ObjectStateEntry entry, EntityKey key)
        {
            if (entry.IsRelationship == false)
            {
                return false;
            }
            return ((EntityKey)entry.UsableValues()[0] == key) || ((EntityKey)entry.UsableValues()[1] == key);
        }

        public static EntityKey OtherEndKey(this ObjectStateEntry relationshipEntry, EntityKey thisEndKey)
        {
            Debug.Assert(relationshipEntry.IsRelationship);
            Debug.Assert(thisEndKey != null);

            if ((EntityKey)relationshipEntry.UsableValues()[0] == thisEndKey)
            {
                return (EntityKey)relationshipEntry.UsableValues()[1];
            }
            else if ((EntityKey)relationshipEntry.UsableValues()[1] == thisEndKey)
            {
                return (EntityKey)relationshipEntry.UsableValues()[0];
            }
            else
            {
                throw new InvalidOperationException("Neither end of the relationship contains the passed in key.");
            }
        }

        public static string OtherEndRole(this ObjectStateEntry relationshipEntry, EntityKey thisEndKey)
        {
            Debug.Assert(relationshipEntry != null);
            Debug.Assert(relationshipEntry.IsRelationship);
            Debug.Assert(thisEndKey != null);

            if ((EntityKey)relationshipEntry.UsableValues()[0] == thisEndKey)
            {
                return relationshipEntry.UsableValues().DataRecordInfo.FieldMetadata[1].FieldType.Name;
            }
            else if ((EntityKey)relationshipEntry.UsableValues()[1] == thisEndKey)
            {
                return relationshipEntry.UsableValues().DataRecordInfo.FieldMetadata[0].FieldType.Name;
            }
            else
            {
                throw new InvalidOperationException("Neither end of the relationship contains the passed in key.");
            }
        }

        //
        // IRelatedEnd methods
        //

        public static bool IsEntityReference(this IRelatedEnd relatedEnd)
        {
            Type relationshipType = relatedEnd.GetType();
            return (relationshipType.GetGenericTypeDefinition() == typeof(EntityReference<>));
        }

        public static EntityKey GetEntityKey(this IRelatedEnd relatedEnd)
        {
            Debug.Assert(relatedEnd.IsEntityReference());
            Type relationshipType = relatedEnd.GetType();
            PropertyInfo pi = relationshipType.GetProperty("EntityKey");
            return (EntityKey)pi.GetValue(relatedEnd, null);
        }

        public static void SetEntityKey(this IRelatedEnd relatedEnd, EntityKey key)
        {
            Debug.Assert(relatedEnd.IsEntityReference());
            Type relationshipType = relatedEnd.GetType();
            PropertyInfo pi = relationshipType.GetProperty("EntityKey");
            pi.SetValue(relatedEnd, key, null);
        }

        public static bool Contains(this IRelatedEnd relatedEnd, EntityKey key)
        {
            foreach (object relatedObject in relatedEnd)
            {
                Debug.Assert(relatedObject is IEntityWithKey);
                if (((IEntityWithKey)relatedObject).EntityKey == key)
                {
                    return true;
                }
            }
            return false;
        }

        //
        // queries over the context
        //

        public static IEnumerable<IEntityWithKey> GetEntities(this ObjectContext context, EntityState state)
        {
            return from e in context.ObjectStateManager.GetObjectStateEntries(state)
                   where e.IsRelationship == false && e.Entity != null
                   select (IEntityWithKey)e.Entity;
        }

        public static IEnumerable<ObjectStateEntry> GetRelationships(this ObjectContext context, EntityState state)
        {
            return from e in context.ObjectStateManager.GetObjectStateEntries(state)
                   where e.IsRelationship == true
                   select e;
        }

        public static IEnumerable<ObjectStateEntry> GetUnchangedManyToManyRelationships(this ObjectContext context)
        {
            return context.GetRelationships(EntityState.Unchanged)
                .Where(e => ((AssociationType)e.EdmType()).IsManyToMany());
        }

        public static IEnumerable<ObjectStateEntry> GetRelationshipsForKey(this ObjectContext context, EntityKey key, EntityState state)
        {
            return context.GetRelationships(state).Where(e => e.IsRelationshipForKey(key));
        }

        public static IEnumerable<IGrouping<IRelatedEnd, ObjectStateEntry>> GetRelationshipsByRelatedEnd(this ObjectContext context,
            IEntityWithKey entity, EntityState state)
        {
            return from entry in context.GetRelationshipsForKey(entity.EntityKey, state)
                   group entry by ((IEntityWithRelationships)entity).RelationshipManager
                                   .GetRelatedEnd(((AssociationType)(entry.EdmType())).Name,
                                                  entry.OtherEndRole(entity.EntityKey));
        }

        //
        // original values
        //

        // Extension method for the ObjectContext which will create an object instance that is essentially equivalent
        // to the original object that was added or attached to the context before any changes were performed.
        // NOTE: This object will have no relationships--just the original value properties.
        public static object CreateOriginalValuesObject(this ObjectContext context, object source)
        {
            // Get the state entry of the source object
            //     NOTE: For now we require the object to implement IEntityWithKey.
            //           This is something we should be able to relax later.
            Debug.Assert(source is IEntityWithKey);
            EntityKey sourceKey = ((IEntityWithKey)source).EntityKey;
            // This method will throw if the key is null or an entry isn't found to match it.  We
            // could throw nicer exceptions, but this will catch the important invalid cases.
            ObjectStateEntry sourceStateEntry = context.ObjectStateManager.GetObjectStateEntry(sourceKey);

            // Return null for added entities & throw an exception for detached ones.  In other cases we can
            // always make a new object with the original values.
            switch (sourceStateEntry.State)
            {
                case EntityState.Added:
                    return null;
                case EntityState.Detached:
                    throw new InvalidOperationException("Can't get original values when detached.");
            }

            // Create target object and add it to the context so that we can easily set properties using 
            // the StateEntry.  Since objects in the added state use temp keys, we know this won't
            // conflict with anything already in the context.
            object target = Activator.CreateInstance(source.GetType());
            string fullEntitySetName = sourceKey.EntityContainerName + "." + sourceKey.EntitySetName;
            context.AddObject(fullEntitySetName, target);
            EntityKey targetKey = context.GetEntityKey(fullEntitySetName, target);
            ObjectStateEntry targetStateEntry = context.ObjectStateManager.GetObjectStateEntry(targetKey);

            // Copy original values from the sourceStateEntry to the targetStateEntry.  This will
            // cause the corresponding properties on the object to be set.
            for (int i = 0; i < sourceStateEntry.OriginalValues.FieldCount; i++)
            {
                // TODO: For best perf we should have a switch on the type here so that we could call 
                // the type-specific methods and avoid boxing.
                targetStateEntry.CurrentValues.SetValue(i, sourceStateEntry.OriginalValues[i]);
            }

            // Detach the object we just created since we only attached it temporarily in order to use 
            // the stateEntry.
            context.Detach(target);

            // Set the EntityKey property on the object (if it implements IEntityWithKey).
            IEntityWithKey targetWithKey = target as IEntityWithKey;
            if (targetWithKey != null)
            {
                targetWithKey.EntityKey = sourceKey;
            }

            return target;
        }

        public static object CreateOriginalValuesObjectWithReferences(this ObjectContext context, object source)
        {
            object target = context.CreateOriginalValuesObject(source);
            EntityKey srcKey = ((IEntityWithKey)source).EntityKey;
            IEntityWithRelationships sourceWithRelationships = source as IEntityWithRelationships;
            if (sourceWithRelationships == null)
            {
                return target;
            }

            foreach (var relationshipGroup in context.GetRelationshipsByRelatedEnd((IEntityWithKey)target,
                                                                                   EntityState.Unchanged | EntityState.Deleted))
            {
                IRelatedEnd tgtRelatedEnd = (IRelatedEnd)relationshipGroup.Key;
                foreach (ObjectStateEntry srcEntry in relationshipGroup)
                {
                    if (tgtRelatedEnd.IsEntityReference())
                    {
                        tgtRelatedEnd.SetEntityKey(srcEntry.OtherEndKey(srcKey));
                    }
                }
            }

            return target;
        }
    }

    #endregion
}
